package com.espirit.weixin.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Properties;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.impl.Indenter;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
import org.codehaus.jackson.util.DefaultPrettyPrinter;
import org.springframework.stereotype.Service;

import com.espirit.https.HttpClientManager;
import com.espirit.weixin.common.Account;
import com.espirit.weixin.common.Menu;
import com.espirit.weixin.common.MenuWrapper;
import com.espirit.weixin.common.Result;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;

@Service
public final class WeixinUtil {

	private static final Log logger = LogFactory.getLog(WeixinUtil.class);

	private static final String CHARSET = "UTF-8";

	private ObjectMapper mapper;

	private AccessToken accessToken;
	
	private JsApiTicket jsApiTicket;
	
	@Resource
	private Config config;
	
	@Resource
	private CacheService cacheService;
	
	private Wrapper<WeixinConfig> weixinConfig;
	
	private final String CONTENT_TYPE="Content-Type";
	
	private final String CONTENT_DISPOSITION="Content-disposition";
	
	private final String TEXT_TYPE="text/plain";
	
	// 获取access_token,有效期为7200秒
	private static final String GET_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

	// 菜单创建（POST） 限100（次/天）
	private static final String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";

	// 菜单查询 (GET)
	private static final String MENU_QUERY_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";

	// 菜单删除 (GET)
	private static final String MENU_DELETE_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";

	// 获取用户基本信息
	private static final String USER_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

	// 上传多媒体文件, 媒体文件在后台保存时间为3天，即3天后media_id失效。
	private final String MEDIA_UPLOAD_URL = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
	
	// 下载多媒体文件,请注意，视频文件不支持下载，调用该接口需http协议。
	private final String MEDIA_DOWNLOAD_URL = "http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID";
	
	// 带微信openId的跳转连接
	private final String OAUTH2_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base#wechat_redirect";
	
	// http://mp.weixin.qq.com/wiki/index.php?title=网页授权获取用户基本信息
	// 通过code换取网页授权access_token
	private final String SSO_GET_OPENID_BY_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";

	private final String QRCODE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
	
	private final String JS_API_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=%s";
	
	public WeixinUtil() {
		// add json object mapper
		mapper = new ObjectMapper();
		mapper.setSerializationConfig(mapper.getSerializationConfig().withSerializationInclusion(Inclusion.NON_NULL).withSerializationInclusion(Inclusion.NON_DEFAULT).withSerializationInclusion(Inclusion.NON_EMPTY));
	}
	
	@PostConstruct
	private void init() {
		weixinConfig = cacheService.newVar(WeixinConfig.class);
	}
	
	/**
	 * Get current weixin configuration
	 * 
	 * @return
	 */
	public WeixinConfig getWeixinConfig() {
		WeixinConfig config = weixinConfig.get();
		if (config == null) {
			config = readWeixinConfig(ContextHolder.get().getSiteName());
			weixinConfig.set(config);
		}
		return config;
	}
	
	public WeixinConfig readWeixinConfig(String siteName) {
		String weixinCfg = config.getPluginDir(siteName) + "weixin.properties";
		File weixinCfgFile = new File(weixinCfg);
		WeixinConfig weixinConfig = null;
		if (weixinCfgFile.exists()) {
			try {
				Properties properties = new Properties();
				properties.load(new FileInputStream(weixinCfgFile));
				
				String appid = properties.getProperty("appid");
				String secret = properties.getProperty("secret");
				String paySignKey = properties.getProperty("paySignKey");
				String partnerId = properties.getProperty("partnerId");
				String partnerKey = properties.getProperty("partnerKey");
				String notifyUrl = properties.getProperty("notifyUrl");
				String redirectUri = properties.getProperty("redirectUri");
				String weiXinSiteName = properties.getProperty("siteName");
				
				weixinConfig = new WeixinConfig();
				weixinConfig.setAppId(appid);
				weixinConfig.setSecret(secret);
				weixinConfig.setPaySignKey(paySignKey);
				weixinConfig.setPartnerId(partnerId);
				weixinConfig.setPartnerKey(partnerKey);
				weixinConfig.setNotifyUrl(notifyUrl);
				weixinConfig.setRedirectUri(redirectUri);
				weixinConfig.setSiteName(weiXinSiteName);
			} catch (IOException ex) {
				logger.error("Failed to load weixin properties", ex);
			}
		}
		
		return weixinConfig;
	}

	public AccessToken getAccessToken(String appid, String secret, boolean reGet) {
		if (accessToken == null || accessToken.isExpired() || reGet) {
			if (appid == null || secret == null) {
				WeixinConfig cfg = getWeixinConfig();
				appid = cfg.getAppId();
				secret = cfg.getSecret();
			}
			
			String url = StringUtils.replaceEach(GET_ACCESS_TOKEN, new String[] { "APPID", "APPSECRET" }, new String[] { appid, secret });
			HttpGet get = HttpClientManager.getGetMethod(url);
			HttpClient client = HttpClientManager.getSSLInstance();
			try {
				HttpResponse response = client.execute(get);
				String json = EntityUtils.toString(response.getEntity(), CHARSET);
				accessToken = jsonToObject(json, AccessToken.class);
				
				if (accessToken.getErrcode() != null) {
					logger.error("Failed to get access token: " + json);
					accessToken = null;
				}
			} catch (IOException e) {
				logger.error("get access token failed." + e.getMessage(), e);
			}
		}
		return accessToken;
	}
	
	/**
	 * 
	 * @param sceneId 32位无符号整数, 0~4294967295 (10位长)
	 * @param sceneStr, 字符串类型，长度限制为1到64，仅永久二维码支持此字段
	 * 
	 * @param accessToken
	 * @param eventCallback
	 * @return
	 */
	public String createQRCode(String sceneFlag, Integer sceneId, String sceneStr, String accessToken) {
		if (accessToken == null) {
			accessToken = getAccessToken(null, null, false).getToken();
		}
		
		boolean isTmp = sceneStr == null;
		String strSceneId;
		if (isTmp) {
			// 确保临时id, 不占用10万以下的值
			if (sceneId < 10000) {
				String str = "" + sceneId;
				String zero = StringUtils.repeat("0", 5 - str.length());
				strSceneId =  sceneFlag + zero + sceneId;
			} else {
				strSceneId =  sceneFlag + sceneId;
			}
		} else {
			// 永久id, 取10万以下的数值
			strSceneId =  "" + sceneId;
			// sceneStr = sceneStr + sceneId
		}
		
		if (logger.isDebugEnabled()) {
			logger.debug("createQRCode, sceneId=" + strSceneId);
		}
		
		
		// 拼装创建菜单的url
		String url = String.format(QRCODE_URL, accessToken);
		// 将菜单对象转换成json字符串
		String qrJson;
		if (isTmp) {
			qrJson = String.format("{\"expire_seconds\": 1800, \"action_name\": \"QR_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": %s}}}", strSceneId);
		} else {
			// QR_LIMIT_STR_SCENE
			//qrJson = String.format("{\"action_name\": \"QR_LIMIT_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": 123}}}", sceneStr);
			qrJson = String.format("{\"action_name\": \"QR_LIMIT_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": %s, \"scene_str\": \"%s\"}}}", strSceneId, sceneStr);
		}

		// 调用接口创建菜单
		HttpPost post = HttpClientManager.getPostMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();
		String qrUrl = null;
		try {
			post.setEntity(new StringEntity(qrJson, CHARSET));
			HttpResponse response = client.execute(post);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			Map<String, String> result = jsonToObject(json, Map.class);

			if (null != result) {
				Object resultCode = result.get("errcode");
				if (resultCode != null) {
					// TODO: correct ? reset the token
					if ("40001".equals(resultCode.toString())) {		// invalid credential
						accessToken = null;
						logger.debug("found error, reset the token");
					}
					
					String errorMsg;
					if (isTmp) {
						errorMsg = "create temporary QR code failed: " + json + ", appId=" + getWeixinConfig().getAppId();
					} else {
						errorMsg = "create LIMIT QR code failed: " + json + ", appId=" + getWeixinConfig().getAppId();
					}
					throw new ApplicationException(errorMsg);
				} else {
					qrUrl = result.get("url");
					
					if (logger.isDebugEnabled()) {
						String ticket = result.get("ticket");
						logger.debug("ticket=" + ticket);
					}
				}
			}
		} catch (IOException e) {
			logger.error("create QRCode failed." + e.getMessage(), e);
		}
		
		// TODO: generate QRcode
		
		return qrUrl;
	}

	public int createMenu(Menu menu, String accessToken) {
		// 拼装创建菜单的url
		String url = MENU_CREATE_URL.replace("ACCESS_TOKEN", accessToken);
		// 将菜单对象转换成json字符串
		String menuJSON = objectToJSON(menu);

		// 调用接口创建菜单
		int resultCode = -1;
		HttpPost post = HttpClientManager.getPostMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();
		try {
			post.setEntity(new StringEntity(menuJSON, CHARSET));
			HttpResponse response = client.execute(post);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			Result result = jsonToObject(json, Result.class);

			if (null != result) {
				resultCode = result.getErrcode();
				if (0 != resultCode) {
					logger.error("create menu failed. errcode:{" + resultCode + "errmsg:{" + result.getErrmsg() + "}");
				}
			}
		} catch (IOException e) {
			logger.error("create menu failed." + e.getMessage(), e);
		}
		return resultCode;
	}

	public Menu getMenu(String accessToken) {
		// 拼装获取菜单的url
		String url = MENU_QUERY_URL.replace("ACCESS_TOKEN", accessToken);

		HttpGet get = HttpClientManager.getGetMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();
		Menu menu = null;
		try {
			HttpResponse response = client.execute(get);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			MenuWrapper wrapper = jsonToObject(json, MenuWrapper.class);
			if (null !=wrapper) {
				menu = wrapper.getMenu();
			}
		} catch (IOException e) {
			logger.error("get menu failed." + e.getMessage(), e);
		}
		return menu;
	}

	public int deleteMenu(String accessToken) {
		// 拼装获取菜单的url
		String url = MENU_DELETE_URL.replace("ACCESS_TOKEN", accessToken);

		HttpGet get = HttpClientManager.getGetMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();
		int resultCode = -1;
		try {
			HttpResponse response = client.execute(get);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			Result result = jsonToObject(json, Result.class);
			if (result!=null) {
				resultCode = result.getErrcode();
			}
		} catch (IOException e) {
			logger.error("get menu failed." + e.getMessage(), e);
		}
		return resultCode;
	}
	
	public void updateMenu(String accessToken,Menu menu) {
		if (menu ==null) {
			return;
		}
		Menu oldMenu  = getMenu(accessToken);
		String oldMenuJSON = oldMenu == null ? null : objectToJSON(oldMenu);
		String newMenuJSON = objectToJSON(menu);
		if (oldMenuJSON != null && oldMenuJSON.equals(newMenuJSON)) {
			return;
		}
		if (StringUtils.isNotBlank(oldMenuJSON)) {
			int result = deleteMenu(accessToken);
			if (result != 0) {
				return;
			}
		}
		createMenu(menu, accessToken);
		
	}
	
	public String upload(String accessToken, MediaType type, String filePath){
		File file  = new File(filePath);
		if (!file.exists()) {
			return null;
		}
		
		String url = StringUtils.replaceEach(MEDIA_UPLOAD_URL, new String[]{"ACCESS_TOKEN","TYPE"},
				new String[]{accessToken,type.name()});
		
		HttpPost post = HttpClientManager.getPostMethod(url);
		try {
			FileEntity reqEntity = new FileEntity(file);
			post.setEntity(reqEntity);
			HttpClient client = HttpClientBuilder.create().build();
			HttpResponse  response = client.execute(post);
			HttpEntity respEntity = response.getEntity();
			String json = IOUtils.toString(respEntity.getContent());
			if (StringUtils.isNotBlank(json)) {
				Result result = jsonToObject(json, Result.class);
				if (result != null) {
					return result.getMediaId();
				}
			}
		} catch (Exception e) {
			logger.error("failed to update media to weixin server."+e.getMessage(),e);
		} 
		return null;
	}
	
	public String download(String accessToken,String mediaId){
		
		String url = StringUtils.replaceEach(MEDIA_DOWNLOAD_URL, new String[]{"ACCESS_TOKEN","MEDIA_ID"}, new String[]{accessToken,mediaId});
		OutputStream os = null;
		try {
			HttpPost post = HttpClientManager.getPostMethod(url);
			HttpClient client = HttpClientBuilder.create().build();
			HttpResponse response = client.execute(post);
			Header typeHeader = response.getFirstHeader(CONTENT_TYPE);
			Header descHeader = response.getFirstHeader(CONTENT_DISPOSITION);
			if (!TEXT_TYPE.equals(typeHeader.getValue()) && descHeader !=null ) {
				String[] descs = descHeader.getValue().split(";")[1].split("=");
				if ("filename".equals(descs[0])) {
					File file = new File(config.getImgDir()+descs[1]);
					HttpEntity entity = response.getEntity();
					InputStream is = entity.getContent();
					os = new FileOutputStream(file);
					byte[] bytes = new byte[1024*8];
					int len  = -1;
					while((len = is.read(bytes))!=-1){
						os.write(bytes, 0, len);
						os.flush();
					}
				}
			}
		} catch (Exception e) {
			logger.error("failed to download media to weixin server."+e.getMessage(),e);
		} finally{
			IOUtils.closeQuietly(os);
		}
		
		return null;
	}

	public String getOauthLink(String appid, String url) {
		if (appid == null) {
			WeixinConfig cfg = getWeixinConfig();
			appid = cfg.getAppId();
		}
		try {
			url = URLEncoder.encode(url, "utf-8");
		} catch (UnsupportedEncodingException e) {
			logger.debug("Failed to encode the url", e);
		}
		return String.format(OAUTH2_URL, appid, url);
	}
	
	public Account getUserInfo(String accessToken, String openId) {
		// 拼装创建获取用户基本信息的url
		String url = StringUtils.replaceEach(USER_INFO_URL, new String[] { "ACCESS_TOKEN", "OPENID" }, new String[] { accessToken, openId });
		// 调用接口获取用户信息
		HttpGet get = HttpClientManager.getGetMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();

		try {
			HttpResponse response = client.execute(get);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			return jsonToObject(json, Account.class);
		} catch (IOException e) {
			logger.error("get user info failed."+e.getMessage(),e);
		}
		return null;
	}
	
	public String getOpenidByToken(String appid, String secret, String code) {
		if (appid == null || secret == null) {
			WeixinConfig config = getWeixinConfig();
			appid = config.getAppId();
			secret = config.getSecret();
		}
		String url = String.format(SSO_GET_OPENID_BY_TOKEN, appid, secret, code);
		HttpGet get = HttpClientManager.getGetMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();

		try {
			HttpResponse response = client.execute(get);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			Map<String, String> result = jsonToObject(json, Map.class);
			String openid = result.get("openid");
			if (openid == null) {
				logger.error("Can't find the openid, return result: " + json);
			}
			return openid;
		} catch (IOException e) {
			logger.error("get user info failed." + e.getMessage(), e);
			return null;
		}
	}
	
	public String getJsApiTicket(String accessToken) {
		if (jsApiTicket != null && !jsApiTicket.isExpired()) {
			return jsApiTicket.getTicket();
		}
		
		// create the JS API ticket
		if (accessToken == null) {
			accessToken = getAccessToken(null, null, false).getToken();
		}
		String url = String.format(JS_API_TICKET, accessToken);
		HttpGet get = HttpClientManager.getGetMethod(url);
		HttpClient client = HttpClientManager.getSSLInstance();

		try {
			HttpResponse response = client.execute(get);
			String json = EntityUtils.toString(response.getEntity(), CHARSET);
			// {"jsapi_ticket":"","expire_time":0}
			jsApiTicket = jsonToObject(json, JsApiTicket.class);
			String ticket = jsApiTicket.getTicket();
			if (ticket == null) {
				logger.error("Failed to get jsApiTicket, [" + jsApiTicket.getErrcode() + "] " + jsApiTicket.getErrmsg());
			}
			return ticket;
		} catch (IOException e) {
			logger.error("get JS API ticket failed." + e.getMessage(), e);
			return null;
		}
	}

	public String objectToJSON(Object object) {
		ObjectWriter jsonWriter = null;
		String text = "";
		try {
			jsonWriter = mapper.writerWithDefaultPrettyPrinter();
			DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
			printer.indentObjectsWith(new Indenter() {
				@Override
				public void writeIndentation(JsonGenerator jg, int level) throws IOException, JsonGenerationException {
					jg.writeRaw('\n');
					for (int i = 0; i < level; i++) {
						jg.writeRaw('\t');
					}
				}

				@Override
				public boolean isInline() {
					return false;
				}
			});
			jsonWriter = jsonWriter.withPrettyPrinter(printer);
			text = jsonWriter.writeValueAsString(object);
		} catch (Exception e) {
			logger.error("format object as json failed!");
		}
		return text;
	}

	public <T> T jsonToObject(String json, Class<T> clazz) {
		T t = null;
		try {
			t = mapper.readValue(json, clazz);
		} catch (Exception e) {
			logger.error("http request error:", e);
		}

		return t;
	}
}
